package com.liaoyifan.core.util;

import com.liaoyifan.core.enums.CoreCodeEnum;
import com.liaoyifan.core.exception.ResultException;
import com.liaoyifan.core.function.Running;
import com.liaoyifan.core.function.Supplying;
import com.liaoyifan.core.valid.Assert;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.redisson.api.RLock;

/**
 * @author liaoyifan
 */
@SuppressWarnings("unused")
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Log4j2
public class Lock {

    // 锁等待时间
    public static final long WAIT_TIME = 10L;
    // 锁过期时间
    public static final long LEASE_TIME = 600L;

    public static void lock(Set<String> keys, Running running) {
        lock(
                keys,
                () -> {
                    running.run();
                    return null;
                },
                WAIT_TIME,
                LEASE_TIME);
    }

    public static <T> T lock(Set<String> keys, Supplying<T> supplying) {
        return lock(keys, supplying, WAIT_TIME, LEASE_TIME);
    }

    public static <T> T lock(
            Set<String> keys, Supplying<T> supplying, long waitTime, long leaseTime) {
        Assert.notEmpty(keys, "keys cannot be empty");
        Assert.isTrue(waitTime > 0, "waitTime must be greater than 0");
        Assert.isTrue(leaseTime > 0, "leaseTime must be greater than 0");
        Assert.notNull(supplying, "supplying cannot be null");
        RLock lock =
                keys.size() == 1
                        ? Redis.getLock(keys.iterator().next())
                        : Redis.getMultiLock(
                                keys.stream().map(Redis::getLock).toArray(RLock[]::new));
        boolean isLocked = false;
        try {
            long start = System.currentTimeMillis();
            isLocked = lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
            long cost = System.currentTimeMillis() - start;
            if (isLocked) {
                log.info(
                        "Locked, Keys: {}, Duration: {}ms, Wait: {}s, Lease: {}s",
                        keys,
                        cost,
                        waitTime,
                        leaseTime);
                return Try.run(supplying);
            }
            throw new ResultException(CoreCodeEnum.RESOURCE_LOCKED);
        } catch (ResultException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            if (isLocked && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}
